import contextlib
from collections import deque
from collections.abc import Callable, Iterable, Iterator
from contextvars import ContextVar
from typing import Any, TypeVar

AnyValue = TypeVar('AnyValue')

AnyItem = TypeVar('AnyItem')
AnyContainer = TypeVar('AnyContainer')

ScanContext = tuple[AnyContainer, deque[AnyItem]]
scan_context_stack_context_var = ContextVar[deque[ScanContext] | None]('scan_context_stack', default=None)


def get_scan_context_stack() -> deque[ScanContext]:
    scan_context_stack_stack = scan_context_stack_context_var.get()
    if scan_context_stack_stack is None:
        scan_context_stack_stack = deque()
        scan_context_stack_context_var.set(scan_context_stack_stack)
    return scan_context_stack_stack


@contextlib.contextmanager
def new_scan_context(container: AnyContainer, items: Iterable[AnyItem]):
    scan_context = container, deque(items)
    scan_context_stack = get_scan_context_stack()
    scan_context_stack.append(scan_context)
    try:
        yield scan_context
    finally:
        scan_context_stack.pop()


def iter_scan_contexts(container: AnyContainer | None = None) -> Iterator[ScanContext]:
    if container is not None:
        for scan_context in iter_scan_contexts():
            scanning_container, _ = scan_context
            if scanning_container == container:
                yield scan_context
        return

    scan_context_stack = get_scan_context_stack()
    yield from reversed(scan_context_stack)


class Postpone(Exception):
    pass


def add_onthefly(
    container: AnyContainer,
    items: Iterable[AnyItem],
):
    for _, scanning_items in iter_scan_contexts(container):
        scanning_items.extend(items)


def ensure_after(
    container: AnyContainer,
    items: Iterable[AnyItem],
):
    for _, scanning_items in iter_scan_contexts(container):
        for item in items:
            if item in scanning_items:
                raise Postpone
        return  # only check the nearest context


def scan_call(
    container: AnyContainer,
    items: Iterable[AnyItem],
    func: Callable[[AnyItem], Any],
):
    with new_scan_context(container, items) as (container, items):
        patience = len(items)
        while items:
            item = items.popleft()
            try:
                func(item)
                patience = len(items)
            except Postpone:
                items.append(item)
                patience -= 1
                if patience <= 0:
                    raise RuntimeError(
                        f"Failed to call the following items "
                        f"due to circular dependencies: {items}")


def scan_agg(
    container: AnyContainer,
    items: Iterable[AnyItem],
    carry: AnyValue,
    func: Callable[[AnyItem, AnyValue], AnyValue],
) -> AnyValue:
    def agg_func(item: AnyItem):
        nonlocal carry
        try:
            carry = func(item, carry)
        except Postpone:
            pass

    scan_call(container, items, agg_func)
    return carry
